/*
 * SingleStreamDownloadController.java
 *
 * Copyright 2007, 2008 Tiberiumlabs
 *
 * This file is part of Tiberiumlabs Lailaps.
 *
 * Tiberiumlabs Lailaps is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Tiberiumlabs Lailaps is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.tiberiumlabs.lailaps.threads.single;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ResourceBundle;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.tiberiumlabs.lailaps.Mediator;
import org.tiberiumlabs.lailaps.actions.ActionFactory;
import org.tiberiumlabs.lailaps.actions.events.DataEvent;
import org.tiberiumlabs.lailaps.actions.events.DataListener;
import org.tiberiumlabs.lailaps.gui.GUIBuilder;
import org.tiberiumlabs.lailaps.gui.elements.ColumnPositions;
import org.tiberiumlabs.lailaps.list.DownloadItem;
import org.tiberiumlabs.lailaps.list.DownloadsListModel;
import org.tiberiumlabs.lailaps.list.StatusConstants;
import org.tiberiumlabs.lailaps.settings.ApplicationSettings;
import org.tiberiumlabs.lailaps.settings.NetworkSettings;
import org.tiberiumlabs.lailaps.settings.Settings;
import org.tiberiumlabs.lailaps.threads.DownloadController;
import org.tiberiumlabs.lailaps.threads.ThreadsMap;
import org.tiberiumlabs.lailaps.toolkit.Constants;
import org.tiberiumlabs.lailaps.toolkit.StateManager;

/**
 *
 * @author <a href="mailto:paranoid.tiberiumlabs@gmail.com">Paranoid</a>
 */
public class SingleStreamDownloadController implements DownloadController, StatusConstants, DataListener, ColumnPositions {

    // <editor-fold defaultstate="collapsed" desc=" i18n ">
    private static final String EXITED_ON_START = "error.onstart";
    private static final String EXCEPTION = "error.exception";
    private static final String SIZE_CHANGED = "error.sizechanged";
    private static final String INFO_FILE_CREATED = "info.filecreated";
    private static final String INFO_CONTINUE = "info.cont";
    private static final String INFO_COMPLETED = "info.completed";
    private static final String INFO_CANCELED = "info.canceled";
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" private objects ">
    private DownloadItem item = null;
    private int row = -1;

    private MethodFacade method = null;
    private InputStream inputStream = null;
    private OutputStream outputStream = null;
    private File targetFile = null;

    private int currentStatus = STOPPED;
    private int needStatus = STOPPED;
    private int bufferSize = networkSettings.getDownloadBufferSize();

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" static ">
    private static final NetworkSettings networkSettings = Settings.getNetworkSettings();
    private static final ApplicationSettings applicationSettings = Settings.getApplicationSettings();
    private static final Log log = LogFactory.getLog(SingleStreamDownloadController.class);
    private static final ResourceBundle bundle = ResourceBundle.getBundle("org/tiberiumlabs/lailaps/resources/messages/download");
    private static final DownloadsListModel model = Mediator.getMediator().getDownloadsListModel();
    private static final StateManager stateManager = Mediator.getMediator().getStateManager();
    private static final String EMPTY_STRING = "";
    private static final String FTP_SCHEME = "ftp://";
    // </editor-fold>

    public SingleStreamDownloadController(DownloadItem item) {
        this.item = item;
    }

    // <editor-fold defaultstate="collapsed" desc=" processing methods ">

    private void processDownload() throws IOException {

        byte[] buffer = new byte[bufferSize];
        int read = 0;
        if (item.getCompletedSize() < 0) {
            item.setCompletedSize(0);
        }
        long completed = item.getCompletedSize();

        item.setStatus(WORKING);
        this.currentStatus = WORKING;

        updateTable(COL_STATUS);

        while (needStatus == currentStatus && (read = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, read);
            completed = completed + read;
            item.setCompletedSize(completed);
            updateSpeed();
            stateManager.incrementDownloadedValue(read);
        }

        if (read == -1) {
            currentStatus = COMPLETED;
        }

    }

    private void prepareDownload() throws IOException {
        String url = item.getUrl();
        if (networkSettings.isProxyEnabled() && networkSettings.getProxyType() == Constants.PROXY_TYPE_HTTP) {
            method = new HttpFacade();
        } else if (url.startsWith(FTP_SCHEME)) {
            this.method = new FtpFacade();
        } else {
            method = new HttpFacade();
        }

        method.setBundle(bundle);
        method.setUrl(url);

        if (item.isTargetFileCreated()) {

            // <editor-fold defaultstate="collapsed" desc=" if our file already created ">
            String filePath = item.getSafeOutputPath() + item.getOutputFile();

            targetFile = new File(filePath);
            if (targetFile.exists()) {
                item.setCompletedSize(targetFile.length());
                method.setOffset(item.getCompletedSize());
                updateTable(COL_COMPLETED);

                method.execute();
                if (needStatus != WORKING) {
                    return;
                }

                long l = method.getLength();
                if (method.getDate() > targetFile.lastModified() || item.getFullSize() - item.getCompletedSize() != l) {
                    log.warn(bundle.getString(SIZE_CHANGED) + ' ' + url);

                    // TODO try to debug if there is any possibility
                    int i = 0;
                    while (!targetFile.renameTo(new File(filePath + ".prev" + (++i))));
                    // we have new file, no reason to append...
                    outputStream = new BufferedOutputStream(new FileOutputStream(targetFile), bufferSize * 2);
                }

                if (outputStream == null) {
                    // it is our old file, need to append
                    outputStream = new BufferedOutputStream(new FileOutputStream(targetFile, true), bufferSize * 2);
                }
                this.inputStream = method.getInputStream();

                // </editor-fold>

                return;
            }
        }

        method.execute();
        if (needStatus != WORKING) {
            return;
        }

        item.setFullSize(method.getLength());
        updateTable(COL_FULLSIZE);
        this.inputStream = method.getInputStream();

        String filePath = item.getSafeOutputPath();
        targetFile = new File(filePath + item.getOutputFile());

        if (!targetFile.exists()) {
            // if file does not exist - need to create full path + file:
            targetFile.getParentFile().mkdirs();
            targetFile.createNewFile();
            item.setTargetFileCreated(true);
            log.info(bundle.getString(INFO_FILE_CREATED) + ' ' + targetFile.getCanonicalPath());
            this.outputStream = new BufferedOutputStream(new FileOutputStream(targetFile), bufferSize*2);
            return;
        }

        // if we still here it seems that file exists... lets find new name!
        String fileName = item.getOutputFile();
        String fileExt = EMPTY_STRING;
        int extIndex = fileName.lastIndexOf('.');
        if (extIndex > 0) {
            fileExt = fileName.substring(extIndex);
            fileName = fileName.substring(0, extIndex) + '_';
        } else {
            fileName = fileName + '_';
        }
        int counter = 0;

        while (targetFile.exists()) {
            counter++;
            targetFile = new File(filePath + fileName + counter + fileExt);
        }

        // if file with such name exists - folder exists to, no need to create...
        targetFile.createNewFile();
        item.setOutputFile(targetFile.getName());
        item.setTargetFileCreated(true);
        updateTable(COL_FILE);
        log.info(bundle.getString(INFO_FILE_CREATED) + ' ' + item.getOutputFile());

        outputStream = new BufferedOutputStream(new FileOutputStream(targetFile), bufferSize*2);

    }

    private void endDownload() {

        long fileTime = 0;

        if (method != null) {
            fileTime = method.getDate();
            try {
                // this method closes inputStream by itself...
                method.close();
            } catch (IOException ex) {
            }
        }

        if (outputStream != null) {
            try {
                outputStream.flush();
                outputStream.close();
            } catch (IOException ex) {
            }
        }

        if (targetFile != null && fileTime > 0 && Settings.getApplicationSettings().isKeepServerTime()) {
            targetFile.setLastModified(fileTime);
        }

        log.info(bundle.getString(INFO_COMPLETED) + ' ' + item.getUrl());

    }

    private void cancelDownload() {

        long fileTime = 0;

        if (method != null) {
            fileTime = method.getDate();
            try {
                method.abort();
            } catch (IOException ex) {
            }
        }

        if (inputStream != null) {
            // may be inputStream already closed by abort, but need to be sure...
            try {
                inputStream.close();
            } catch (Exception ex) {
            }
        }

        if (outputStream != null) {
            try {
                outputStream.flush();
                outputStream.close();
            } catch (IOException ex) {
            }
        }

        if (targetFile != null && fileTime > 0 && Settings.getApplicationSettings().isKeepServerTime()) {
            targetFile.setLastModified(fileTime);
        }

        log.info(bundle.getString(INFO_CANCELED) + ' ' + item.getUrl());
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" DownloadController implementation ">

    @Override
    public void start(int row) {
        this.row = row;
        this.needStatus = WORKING;
        this.currentStatus = WAITING;
        item.setStatus(WAITING);
        updateTable(COL_STATUS);

        ThreadsMap.execute(this);
    }

    @Override
    public void stop() {
        needStatus = STOPPED;
        //item.setStatus(STOPPED);
        if (currentStatus == PAUSED) {
            cancelDownload();
            item.setStatus(STOPPED);
        }
        //updateTable(COL_STATUS);
        // TODO check do we need to refresh table in this method
    }

    @Override
    public void pause() {
        needStatus = PAUSED;
    }

    // </editor-fold>

    @Override
    public void dataChanged(DataEvent event) {
        System.out.println(event);
        // if row of event is lower in table - our position did not changed
        // TODO do something if row == row from event
        if (row < event.getFirstRow()) {
            return;
        }

        // if last row of event is higher than our - just move out row for count
        if (event.getLastRow() < row) {
            if (event.getType() == DataEvent.INSERTED) {
                row = row + event.getCount();
                return;
            }
            if (event.getType() == DataEvent.REMOVED) {
                row = row - event.getCount();
                return;
            }
        }

        // we somewhere inside of shifted range
        // MOVED type is here too - there is no need to check it again
        row = model.indexOfItem(item);
    }

    @Override
    public void run() {
        if (needStatus != WORKING) {
            log.info(bundle.getString(EXITED_ON_START));
            item.setStatus(STOPPED);
            updateTable(COL_STATUS);
            return;
        }

        try {

            item.setStatus(PREPARING);
            updateTable(COL_STATUS);

            // calculate current row if it is not set
            if (row < 0) {
                row = model.indexOfItem(item);
            }

            prepareDownload();

            updateTable(COL_ALL);

            if (needStatus != WORKING) {
                return;
            }

            processDownload();

            if (currentStatus == COMPLETED) {
                endDownload();
                item.setStatus(COMPLETED);
                ActionFactory.done(item, false);
                return;
            }

            if (needStatus != PAUSED) {
                cancelDownload();
            }

            item.setStatus(needStatus);
            currentStatus = needStatus;

        } catch (Exception e) {

            log.error(bundle.getString(EXCEPTION), e);
            cancelDownload();
            if (needStatus != STOPPED) {
                item.setStatus(ERROR);
            }
            ActionFactory.done(item, true);

        } finally {

            updateTable(COL_ALL);

        }
    }

    // <editor-fold defaultstate="collapsed" desc=" downloadsTable updates ">
    private GUIBuilder guiBuilder = Mediator.getMediator().getGuiBuilder();

    private long lastUpdate = 0;
    private void updateSpeed() {
        long t = System.currentTimeMillis();
        if (t - lastUpdate >= 240 && guiBuilder.isTableVisible()) {
            // TODO make repaint not in downloading thread, but in some common class like SwingWorker
            guiBuilder.getTable().repaintCells(row, COL_COMPLETED, COL_SPEED, COL_PROGRESS, COL_SIZEPROGRESS, COL_TIMEELAPSED, COL_TIMEESTIMATED);
            lastUpdate = t;
        }
    }
    private void updateTable(int column) {
        if (guiBuilder.isTableVisible()) {
            guiBuilder.getTable().repaintCell(row, column);
        }
    }

    // </editor-fold>

    public int compareTo(DownloadController o) {
        return this.row - o.getRow();
    }

    public int getRow() {
        return this.row;
    }

}